重载函数名

  最常见的情况是,应该给不同函数以不同的名字。但是,当某些函数在不同类型的对象上执行概念上相同的工作时,能给它们取相同的名字就更方便了。将同一名字用于在不同类型上操作的函数的情况称为重载。这一技术早已用于C++中的基本运算。例如,对于加法只存在一个名字,+,然而它却可以用于做整数、浮点数和指针值的加法。这种思想很容易扩充到程序员定义的函数。例如,

void print(int);            // 打印int
void print(const char*);    // 打印C风格的字符串

如果只考虑编译器,那么具有同样名字的函数之间共同的东西就只是这个名字。按照推测,这些函数应该在某种意义上是相互类似的,但语言无法在这方面限制或者帮助程序员。这样,重载函数名从根本上说就是一种记法上的方便。对那些有着习用名字的函数,如sqrt、print和open等,这种方便性是很重要的。当一个名字在语义上很重要时,这种方便将是根本性的。这种情况出现在例如+、-、* 和 << 等运算符,构造函数(11.7节),以及通用型程序设计(2.7.2节、第18章)等许多情况中。当一个函数f被调用时,编译器就必须弄清究竟应该调用具有名字f的哪一个函数。为了完成这项工作,它需要将实际参数的类型与所有名字为f的函数的形式参数的类型相比较。基本思想是去调用其中的那个在参数上匹配得最好的函数,如果不存在匹配得最好的函数,就给出一个编译错误。例如,

    void print(double);
    void print(long);

    void f()
    {
        print(1L);            // print(long)
        print(1.0);           // print(double)
        print(1);             // 错误❌,歧义性:print(long(1))或print(double(1))?
    }

要从一集重载的函数中找到应实际调用的那个正确版本,就需要找到在参数表达式的类型和函数的(形式)参数类型之间的最好匹配。为了尽可能接近我们关于怎样做最合理的观念,需要按顺序检查下面的一系列判断推测:

1)、准确匹配;也就是说,无须任何转换或者只须做平凡转换(例如,数组名到指针,函数名到函数指针,T到const T等)的匹配。
2)、利用提升的匹配;即包括整数提升(bool到int,char到int,short到int以及它们的无符号版本;C.6.1节)以及从float到double的提升。
3)、利用标准转换(例如,int到double,double到int,double到long double,Derived* 到Base*(12.2节),T*到void*(5.6节),int到unsigned int;C.6节)的匹配
4)、利用用户定义转换(11.4节)的匹配。
5)、利用在函数声明中的省略号...(7.6节)的匹配。

如果在能找到匹配的某个最高的层次上同时发现了两个匹配,这个调用将作为存在歧义而被拒绝。这些解析规则经过了仔细的推敲,主要是为了将C和C++关于内部数值类型的精细规则(C.6节)也一并考虑在内。例如,

    void print(int);
    void print(const char*);
    void print(double);
    void print(long);
    void print(char);

    void h(char c, int i, short s, float f)
    {
        print(c);        // 准确匹配:调用print(char)
        print(i);        // 准确匹配:调用print(int)
        print(s);        // 整数提升:调用print(int)
        print(f);        // float到double的提升:调用print(double)

        print('a');      // 准确匹配:调用print(char)
        print(49);       // 准确匹配:调用print(int)
        print(0);        // 准确匹配:调用print(int)
        print("a");      // 准确匹配:调用print(const char*)
    }

调用print(0)将激活print(int),是因为0是int。调用print('a')激活print(char),因为'a'是char(4.3.1节)。将提升和转换分开的原因是我们希望能更偏向于提升,例如char到int;而不是不那么安全的转换,如int到char。

  重载解析与被考虑的函数声明的顺序无关。

  相对而言,重载所依赖的这组规则是比较复杂的,但程序员却很少会对哪个函数被调用感到吃惊。那么,为什么呢?请考虑一下重载的替代方式。我们经常需要对几个类型的对象执行类似操作。如果没有重载,我们就必须用几个不同的名字去定义几个函数:

    void print_int(int);
    void print_char(char);
    void print_string(const char*);    // C风格的字符串

    void g(int i, char c, const char* p, double d)
    {
        print_int(i);                  // ok
        print_char(c);                 // ok
        print_string(p);               // ok

        print_int(c);                  // 可以吗?调用print_int(int(c))
        print_char(i);                 // 可以吗?调用print_char(char(i))
        print_string(i);               // 错误❌
        print_int(d);                  // 可以吗?调用print_int(int(d))
    }

与重载的print()相比,我们必须同时记住几个名字,而且要记住如何正确地使用它们。这会令人厌倦,页会挫败通用型程序设计(2.7.2节)的企图,并促使程序员去注意相对低级的类型问题。因为不存在重载函数,所有标准转换都将被用于这些函数的参数。这样做也可能引起错误。在上面的例子里,这种情况的潜台词是,在这里的四个采用了“错误”参数的调用中,编译器只能捕捉到其中的一个。由此看,重载还能增加编译器拒绝不合适参数的机会。

🔚